浅谈vue的第一个commit分析


Posted in Javascript onJune 08, 2020

为什么写这篇vue的分析文章?

对于天资愚钝的前端(我)来说,阅读源码是件不容易的事情,毕竟有时候看源码分析的文章都看不懂。每次看到大佬们用了1~2年的vue就能掌握原理,甚至精通源码,再看看自己用了好几年都还在基本的使用阶段,心中总是羞愧不已。如果一直满足于基本的业务开发,怕是得在初级水平一直待下去了吧。所以希望在学习源码的同时记录知识点,可以让自己的理解和记忆更加深刻,也方便将来查阅。

目录结构

本文以vue的第一次 commit a879ec06 作为分析版本

├── build
│  └── build.js        // `rollup` 打包配置
├── dist            
│  └── vue.js  
├── package.json
├── src            // vue源码目录
│  ├── compiler        // 将vue-template转化为render函数
│  │  ├── codegen.js     // 递归ast提取指令,分类attr,style,class,并生成render函数
│  │  ├── html-parser.js   // 通过正则匹配将html字符串转化为ast
│  │  ├── index.js      // compile主入口
│  │  └── text-parser.js   // 编译{{}}
│  ├── config.js       // 对于vue的全局配置文件
│  ├── index.js        // 主入口
│  ├── index.umd.js      // 未知(应该是umd格式的主入口)
│  ├── instance        // vue实例函数
│  │  └── index.js      // 包含了vue实例的初始化,compile,data代理,methods代理,watch数据,执行渲染
│  ├── observer        // 数据订阅发布的实现
│  │  ├── array.js      // 实现array变异方法,$set $remove 实现
│  │  ├── batcher.js     // watch执行队列的收集,执行
│  │  ├── dep.js       // 订阅中心实现
│  │  ├── index.js      // 数据劫持的实现,收集订阅者
│  │  └── watcher.js     // watch实现,订阅者
│  ├── util          // 工具函数
│  │  ├── component.js
│  │  ├── debug.js
│  │  ├── dom.js
│  │  ├── env.js       // nexttick实现
│  │  ├── index.js
│  │  ├── lang.js
│  │  └── options.js
│  └── vdom
│    ├── dom.js       // dom操作的封装
│    ├── h.js        // 节点数据分析(元素节点,文本节点)
│    ├── index.js      // vdom主入口
│    ├── modules      // 不同属性处理函数
│    │  ├── attrs.js    // 普通attr属性处理
│    │  ├── class.js    // class处理
│    │  ├── events.js   // event处理
│    │  ├── props.js    // props处理
│    │  └── style.js    // style处理
│    ├── patch.js      // node树的渲染,包括节点的加减更新处理,及对应attr的处理
│    └── vnode.js      // 返回最终的节点数据
└── webpack.config.js     // webpack配置

从template到html的过程分析

我们的代码是从new Vue()开始的,Vue的构造函数如下:

constructor (options) {
 // options就是我们对于vue的配置
 this.$options = options
 this._data = options.data
 // 获取元素html,即template
 const el = this._el = document.querySelector(options.el)
 // 编译模板 -> render函数
 const render = compile(getOuterHTML(el))
 this._el.innerHTML = ''
 // 实例代理data数据
 Object.keys(options.data).forEach(key => this._proxy(key))
 // 将method的this指向实例
 if (options.methods) {
  Object.keys(options.methods).forEach(key => {
   this[key] = options.methods[key].bind(this)
  })
 }
 // 数据观察
 this._ob = observe(options.data)
 this._watchers = []
 // watch数据及更新
 this._watcher = new Watcher(this, render, this._update)
 // 渲染函数
 this._update(this._watcher.value)
}

当我们初始化项目的时候,即会执行构造函数,该函数向我们展示了vue初始化的主线:编译template字符串 => 代理data数据/methods的this绑定 => 数据观察 => 建立watch及更新渲染

1. 编译template字符串

const render = compile(getOuterHTML(el))

其中compile的实现如下:

export function compile (html) {
 html = html.trim()
 // 对编译结果缓存
 const hit = cache[html]
 // parse函数在parse-html中定义,其作用是把我们获取的html字符串通过正则匹配转化为ast,输出如下 {tag: 'div', attrs: {}, children: []}
 return hit || (cache[html] = generate(parse(html)))
}

接下来看看generate函数,ast通过genElement的转化生成了构建节点html的函数,在genElement将对if for 等进行判断并转化( 指令的具体处理将在后面做分析,先关注主流程代码),最后都会执行genData函数

// 生成节点主函数
export function generate (ast) {
 const code = genElement(ast)
 // 执行code代码,并将this作为code的global对象。所以我们在template中的变量将指向为实例的属性 {{name}} -> this.name 
 return new Function (`with (this) { return $[code]}`)
}

// 解析单个节点 -> genData
function genElement (el, key) {
 let exp
 // 指令的实现,实际就是在模板编译时实现的
 if (exp = getAttr(el, 'v-for')) {
  return genFor(el, exp)
 } else if (exp = getAttr(el, 'v-if')) {
  return genIf(el, exp)
 } else if (el.tag === 'template') {
  return genChildren(el)
 } else {
  // 分别为 tag 自身属性 子节点数据
  return `__h__('${ el.tag }', ${ genData(el, key) }, ${ genChildren(el) })`
 }
}

我们可以看看在genData中都做了什么。上面的parse函数将html字符串转化为ast,而在genData中则将节点的attrs数据进一步处理,例如class -> renderClass style class props attr 分类。在这里可以看到 bind 指令的实现,即通过正则匹配 : 和 bind,如果匹配则把相应的 value值转化为 (value)的形式,而不匹配的则通过JSON.stringify()转化为字符串('value')。最后输出attrs的(key-value),在这里得到的对象是字符串形式的,例如(value)等也仅仅是将变量名,而在generate中通过new Function进一步通过(this.value)得到变量值。

function genData (el, key) {
 // 没有属性返回空对象
 if (!el.attrs.length) {
  return '{}'
 }
 // key
 let data = key ? `{key:${ key },` : `{`
 // class处理
 if (el.attrsMap[':class'] || el.attrsMap['class']) {
  data += `class: _renderClass(${ el.attrsMap[':class'] }, "${ el.attrsMap['class'] || '' }"),`
 }
 // attrs
 let attrs = `attrs:{`
 let props = `props:{`
 let hasAttrs = false
 let hasProps = false
 for (let i = 0, l = el.attrs.length; i < l; i++) {
  let attr = el.attrs[i]
  let name = attr.name
  // bind属性
  if (bindRE.test(name)) {
   name = name.replace(bindRE, '')
   if (name === 'class') {
    continue
   // style处理
   } else if (name === 'style') {
    data += `style: ${ attr.value },`
   // props属性处理
   } else if (mustUsePropsRE.test(name)) {
    hasProps = true
    props += `"${ name }": (${ attr.value }),` 
   // 其他属性
   } else {
    hasAttrs = true
    attrs += `"${ name }": (${ attr.value }),`
   }
  // on指令,未实现
  } else if (onRE.test(name)) {
   name = name.replace(onRE, '')
  // 普通属性
  } else if (name !== 'class') {
   hasAttrs = true
   attrs += `"${ name }": (${ JSON.stringify(attr.value) }),`
  }
 }
 if (hasAttrs) {
  data += attrs.slice(0, -1) + '},'
 }
 if (hasProps) {
  data += props.slice(0, -1) + '},'
 }
 return data.replace(/,$/, '') + '}'
}

而对于genChildren,我们可以猜到就是对ast中的children进行遍历调用genElement,实际上在这里还包括了对文本节点的处理。

// 遍历子节点 -> genNode
function genChildren (el) {
 if (!el.children.length) {
  return 'undefined'
 }
 // 对children扁平化处理
 return '__flatten__([' + el.children.map(genNode).join(',') + '])'
}

function genNode (node) {
 if (node.tag) {
  return genElement(node)
 } else {
  return genText(node)
 }
}

// 解析{{}}
function genText (text) {
 if (text === ' ') {
  return '" "'
 } else {
  const exp = parseText(text)
  if (exp) {
   return 'String(' + escapeNewlines(exp) + ')'
  } else {
   return escapeNewlines(JSON.stringify(text))
  }
 }
}

genText处理了text及换行,在parseText函数中利用正则解析{{}},输出字符串(value)形式的字符串。

现在我们再看看__h__('${ el.tag }', ${ genData(el, key) }, ${ genChildren(el) })中__h__函数

// h 函数利用上面得到的节点数据得到 vNode对象 => 虚拟dom
export default function h (tag, b, c) {
 var data = {}, children, text, i
 if (arguments.length === 3) {
  data = b
  if (isArray(c)) { children = c }
  else if (isPrimitive(c)) { text = c }
 } else if (arguments.length === 2) {
  if (isArray(b)) { children = b }
  else if (isPrimitive(b)) { text = b }
  else { data = b }
 }
 if (isArray(children)) {
  // 子节点递归处理
  for (i = 0; i < children.length; ++i) {
   if (isPrimitive(children[i])) children[i] = VNode(undefined, undefined, undefined, children[i])
  }
 }
 // svg处理
 if (tag === 'svg') {
  addNS(data, children)
 }
 // 子节点为文本节点
 return VNode(tag, data, children, text, undefined)
}

到此为止,我们分析了const render = compile(getOuterHTML(el)),从el的html字符串到render函数都是怎么处理的。

2. 代理data数据/methods的this绑定

// 实例代理data数据
Object.keys(options.data).forEach(key => this._proxy(key))
// 将method的this指向实例
if (options.methods) {
 Object.keys(options.methods).forEach(key => {
  this[key] = options.methods[key].bind(this)
 })
}

实例代理data数据的实现比较简单,就是利用了对象的setter和getter,读取this数据时返回data数据,在设置this数据时同步设置data数据

_proxy (key) {
 if (!isReserved(key)) {
  // need to store ref to self here
  // because these getter/setters might
  // be called by child scopes via
  // prototype inheritance.
  var self = this
  Object.defineProperty(self, key, {
   configurable: true,
   enumerable: true,
   get: function proxyGetter () {
    return self._data[key]
   },
   set: function proxySetter (val) {
    self._data[key] = val
   }
  })
 }
}

3. Obaerve的实现

Observe的实现原理在很多地方都有分析,主要是利用了Object.defineProperty()来建立对数据更改的订阅,在很多地方也称之为数据劫持。下面我们来学习从零开始建立这样一个数据的订阅发布体系。

从简单处开始,我们希望有个函数可以帮我们监听数据的改变,每当数据改变时执行特定回调函数

function observe(data, callback) {
 if (!data || typeof data !== 'object') {
  return
 }

 // 遍历key
 Object.keys(data).forEach((key) => {
  let value = data[key];

  // 递归遍历监听深度变化
  observe(value, callback);

  // 监听单个可以的变化
  Object.defineProperty(data, key, {
   configurable: true,
   enumerable: true,
   get() {
    return value;
   },
   set(val) {
    if (val === value) {
     return
    }

    value = val;

    // 监听新的数据
    observe(value, callback);
    
    // 数据改变的回调
    callback();
   }
  });
 });
}

// 使用observe函数监听data
const data = {};
observe(data, () => {
 console.log('data修改');
})

上面我们实现了一个简单的observe函数,只要我们将编译函数作为callback传入,那么每次数据更改时都会触发回调函数。但是我们现在不能为单独的key设置监听及回调函数,只能监听整个对象的变化执行回调。下面我们对函数进行改进,达到为某个key设置监听及回调。同时建立调度中心,让整个订阅发布模式更加清晰。

// 首先是订阅中心
class Dep {
 constructor() {
  this.subs = []; // 订阅者数组
 }

 addSub(sub) {
  // 添加订阅者
  this.subs.push(sub);
 }

 notify() {
  // 发布通知
  this.subs.forEach((sub) => {
   sub.update();
  });
 }
}

// 当前订阅者,在getter中标记
Dep.target = null;

// 订阅者
class Watch {
 constructor(express, cb) {
  this.cb = cb;
  if (typeof express === 'function') {
   this.expressFn = express;
  } else {
   this.expressFn = () => {
    return new Function(express)();
   }
  }
  
  this.get();
 }

 get() {
  // 利用Dep.target存当前订阅者
  Dep.target = this;
  // 执行表达式 -> 触发getter -> 在getter中添加订阅者
  this.expressFn();
  // 及时置空
  Dep.taget = null;
 }

 update() {
  // 更新
  this.cb();
 }

 addDep(dep) {
  // 添加订阅
  dep.addSub(this);
 }
}

// 观察者 建立观察
class Observe {
 constructor(data) {
  if (!data || typeof data !== 'object') {
   return
  }
 
  // 遍历key
  Object.keys(data).forEach((key) => {
   // key => dep 对应
   const dep = new Dep();
   let value = data[key];
 
   // 递归遍历监听深度变化
   const observe = new Observe(value);
 
   // 监听单个可以的变化
   Object.defineProperty(data, key, {
    configurable: true,
    enumerable: true,
    get() {
     if (Dep.target) {
      const watch = Dep.target;
      watch.addDep(dep);
     }
     return value;
    },
    set(val) {
     if (val === value) {
      return
     }
 
     value = val;
 
     // 监听新的数据
     new Observe(value);
     
     // 数据改变的回调
     dep.notify();
    }
   });
  });
 }
}

// 监听数据中某个key的更改
const data = {
 name: 'xiaoming',
 age: 26
};

const observe = new Observe(data);

const watch = new Watch('data.age', () => {
 console.log('age update');
});

data.age = 22

现在我们实现了订阅中心,订阅者,观察者。观察者监测数据的更新,订阅者通过订阅中心订阅数据的更新,当数据更新时,观察者会告诉订阅中心,订阅中心再逐个通知所有的订阅者执行更新函数。到现在为止,我们可以大概猜出vue的实现原理:

  1. 建立观察者观察data数据的更改 (new Observe)
  2. 在编译的时候,当某个代码片段或节点依赖data数据,为该节点建议订阅者,订阅data中某些数据的更新(new Watch)
  3. 当dada数据更新时,通过订阅中心通知数据更新,执行节点更新函数,新建或更新节点(dep.notify())

上面是我们对vue实现原理订阅发布模式的基本实现,及编译到更新过程的猜想,现在我们接着分析vue源码的实现:
在实例的初始化中

// ...
// 为数据建立数据观察
this._ob = observe(options.data)
this._watchers = []
// 添加订阅者 执行render 会触发 getter 订阅者订阅更新,数据改变触发 setter 订阅中心通知订阅者执行 update
this._watcher = new Watcher(this, render, this._update)
// ...

vue中数据观察的实现

// observe函数
export function observe (value, vm) {
 if (!value || typeof value !== 'object') {
  return
 }
 if (
  hasOwn(value, '__ob__') &&
  value.__ob__ instanceof Observer
 ) {
  ob = value.__ob__
 } else if (
  shouldConvert &&
  (isArray(value) || isPlainObject(value)) &&
  Object.isExtensible(value) &&
  !value._isVue
 ) {
  // 为数据建立观察者
  ob = new Observer(value)
 }
 // 存储关联的vm
 if (ob && vm) {
  ob.addVm(vm)
 }
 return ob
}

// => Observe 函数
export function Observer (value) {
 this.value = value
 // 在数组变异方法中有用
 this.dep = new Dep()
 // observer实例存在__ob__中
 def(value, '__ob__', this)
 if (isArray(value)) {
  var augment = hasProto
   ? protoAugment
   : copyAugment
  // 数组遍历,添加变异的数组方法
  augment(value, arrayMethods, arrayKeys)
  // 对数组的每个选项调用observe函数
  this.observeArray(value)
 } else {
  // walk -> convert -> defineReactive -> setter/getter
  this.walk(value)
 }
}

// => walk
Observer.prototype.walk = function (obj) {
 var keys = Object.keys(obj)
 for (var i = 0, l = keys.length; i < l; i++) {
  this.convert(keys[i], obj[keys[i]])
 }
}

// => convert
Observer.prototype.convert = function (key, val) {
 defineReactive(this.value, key, val)
}

// 重点看看defineReactive
export function defineReactive (obj, key, val) {
 // key对应的的订阅中心
 var dep = new Dep()

 var property = Object.getOwnPropertyDescriptor(obj, key)
 if (property && property.configurable === false) {
  return
 }

 // 兼容原有setter/getter
 // cater for pre-defined getter/setters
 var getter = property && property.get
 var setter = property && property.set

 // 实现递归监听属性 val = obj[key]
 // 深度优先遍历 先为子属性设置 reactive
 var childOb = observe(val)
 // 设置 getter/setter
 Object.defineProperty(obj, key, {
  enumerable: true,
  configurable: true,
  get: function reactiveGetter () {
   var value = getter ? getter.call(obj) : val
   // Dep.target 为当前 watch 实例
   if (Dep.target) {
    // dep 为 obj[key] 对应的调度中心 dep.depend 将当前 wtcher 实例添加到调度中心
    dep.depend()
    if (childOb) {
     // childOb.dep 为 obj[key] 值 val 对应的 observer 实例的 dep
     // 实现array的变异方法和$set方法订阅
     childOb.dep.depend()
    }

    // TODO: 此处作用未知?
    if (isArray(value)) {
     for (var e, i = 0, l = value.length; i < l; i++) {
      e = value[i]
      e && e.__ob__ && e.__ob__.dep.depend()
     }
    }
   }
   return value
  },
  set: function reactiveSetter (newVal) {
   var value = getter ? getter.call(obj) : val
   // 通过 getter 获取 val 判断是否改变
   if (newVal === value) {
    return
   }
   if (setter) {
    setter.call(obj, newVal)
   } else {
    val = newVal
   }
   // 为新值设置 reactive
   childOb = observe(newVal)
   // 通知key对应的订阅中心更新
   dep.notify()
  }
 })
}

订阅中心的实现

let uid = 0

export default function Dep () {
 this.id = uid++
 // 订阅调度中心的watch数组
 this.subs = []
}

// 当前watch实例
Dep.target = null

// 添加订阅者
Dep.prototype.addSub = function (sub) {
 this.subs.push(sub)
}

// 移除订阅者
Dep.prototype.removeSub = function (sub) {
 this.subs.$remove(sub)
}

// 订阅
Dep.prototype.depend = function () {
 // Dep.target.addDep(this) => this.addSub(Dep.target) => this.subs.push(Dep.target)
 Dep.target.addDep(this)
}

// 通知更新
Dep.prototype.notify = function () {
 // stablize the subscriber list first
 var subs = this.subs.slice()
 for (var i = 0, l = subs.length; i < l; i++) {
  // subs[i].update() => watch.update()
  subs[i].update()
 }
}

订阅者的实现

export default function Watcher (vm, expOrFn, cb, options) {
 // mix in options
 if (options) {
  extend(this, options)
 }
 var isFn = typeof expOrFn === 'function'
 this.vm = vm
 // vm 的 _watchers 包含了所有 watch
 vm._watchers.push(this)
 this.expression = expOrFn
 this.cb = cb
 this.id = ++uid // uid for batching
 this.active = true
 this.dirty = this.lazy // for lazy watchers
 // deps 一个 watch 实例可以对应多个 dep
 this.deps = []
 this.newDeps = []
 this.depIds = Object.create(null)
 this.newDepIds = null
 this.prevError = null // for async error stacks
 // parse expression for getter/setter
 if (isFn) {
  this.getter = expOrFn
  this.setter = undefined
 } else {
  warn('vue-lite only supports watching functions.')
 }
 this.value = this.lazy
  ? undefined
  : this.get()
 this.queued = this.shallow = false
}

Watcher.prototype.get = function () {
 this.beforeGet()
 var scope = this.scope || this.vm
 var value
 try {
  // 执行 expOrFn,此时会触发 getter => dep.depend() 将watch实例添加到对应 obj[key] 的 dep
  value = this.getter.call(scope, scope)
 }
 if (this.deep) {
  // 深度watch
  // 触发每个key的getter watch实例将对应多个dep
  traverse(value)
 }
 // ...
 this.afterGet()
 return value
}

// 触发getter,实现订阅
Watcher.prototype.beforeGet = function () {
 Dep.target = this
 this.newDepIds = Object.create(null)
 this.newDeps.length = 0
}

// 添加订阅
Watcher.prototype.addDep = function (dep) {
 var id = dep.id
 if (!this.newDepIds[id]) {
  // 将新出现的dep添加到newDeps中
  this.newDepIds[id] = true
  this.newDeps.push(dep)
  // 如果已在调度中心,不再重复添加
  if (!this.depIds[id]) {
   // 将watch添加到调度中心的数组中
   dep.addSub(this)
  }
 }
}

Watcher.prototype.afterGet = function () {
 // 切除key的getter联系
 Dep.target = null
 var i = this.deps.length
 while (i--) {
  var dep = this.deps[i]
  if (!this.newDepIds[dep.id]) {
   // 移除不在expOrFn表达式中关联的dep中watch的订阅
   dep.removeSub(this)
  }
 }
 this.depIds = this.newDepIds
 var tmp = this.deps
 this.deps = this.newDeps
 // TODO: 既然newDeps最终会被置空,这边赋值的意义在于?
 this.newDeps = tmp
}

// 订阅中心通知消息更新
Watcher.prototype.update = function (shallow) {
 if (this.lazy) {
  this.dirty = true
 } else if (this.sync || !config.async) {
  this.run()
 } else {
  // if queued, only overwrite shallow with non-shallow,
  // but not the other way around.
  this.shallow = this.queued
   ? shallow
    ? this.shallow
    : false
   : !!shallow
  this.queued = true
  // record before-push error stack in debug mode
  /* istanbul ignore if */
  if (process.env.NODE_ENV !== 'production' && config.debug) {
   this.prevError = new Error('[vue] async stack trace')
  }
  // 添加到待执行池
  pushWatcher(this)
 }
}

// 执行更新回调
Watcher.prototype.run = function () {
 if (this.active) {
  var value = this.get()
  if (
   ((isObject(value) || this.deep) && !this.shallow)
  ) {
   // set new value
   var oldValue = this.value
   this.value = value
   var prevError = this.prevError
   // ...
   this.cb.call(this.vm, value, oldValue)
  }
  this.queued = this.shallow = false
 }
}

Watcher.prototype.depend = function () {
 var i = this.deps.length
 while (i--) {
  this.deps[i].depend()
 }
}

wtach回调执行队列

在上面我们可以发现,watch在收到信息更新执行update时。如果非同步情况下会执行pushWatcher(this)将实例推入执行池中,那么在何时会执行回调函数,如何执行呢?我们一起看看pushWatcher的实现。

// batch.js
var queueIndex
var queue = []
var userQueue = []
var has = {}
var circular = {}
var waiting = false
var internalQueueDepleted = false

// 重置执行池
function resetBatcherState () {
 queue = []
 userQueue = []
 // has 避免重复
 has = {}
 circular = {}
 waiting = internalQueueDepleted = false
}

// 执行执行队列
function flushBatcherQueue () {
 runBatcherQueue(queue)
 internalQueueDepleted = true
 runBatcherQueue(userQueue)
 resetBatcherState()
}

// 批量执行
function runBatcherQueue (queue) {
 for (queueIndex = 0; queueIndex < queue.length; queueIndex++) {
  var watcher = queue[queueIndex]
  var id = watcher.id
  // 执行后置为null
  has[id] = null
  watcher.run()
  // in dev build, check and stop circular updates.
  if (process.env.NODE_ENV !== 'production' && has[id] != null) {
   circular[id] = (circular[id] || 0) + 1
   if (circular[id] > config._maxUpdateCount) {
    warn(
     'You may have an infinite update loop for watcher ' +
     'with expression "' + watcher.expression + '"',
     watcher.vm
    )
    break
   }
  }
 }
}

// 添加到执行池
export function pushWatcher (watcher) {
 var id = watcher.id
 if (has[id] == null) {
  if (internalQueueDepleted && !watcher.user) {
   // an internal watcher triggered by a user watcher...
   // let's run it immediately after current user watcher is done.
   userQueue.splice(queueIndex + 1, 0, watcher)
  } else {
   // push watcher into appropriate queue
   var q = watcher.user
    ? userQueue
    : queue
   has[id] = q.length
   q.push(watcher)
   // queue the flush
   if (!waiting) {
    waiting = true
    // 在nextick中执行
    nextTick(flushBatcherQueue)
   }
  }
 }
}

4. patch实现

上面便是vue中数据驱动的实现原理,下面我们接着回到主流程中,在执行完watch后,便执行this._update(this._watcher.value)开始节点渲染

// _update => createPatchFunction => patch => patchVnode => (dom api)

// vtree是通过compile函数编译的render函数执行的结果,返回了当前表示当前dom结构的对象(虚拟节点树)
_update (vtree) {
 if (!this._tree) {
  // 第一次渲染
  patch(this._el, vtree)
 } else {
  patch(this._tree, vtree)
 }
 this._tree = vtree
}

// 在处理节点时,需要针对class,props,style,attrs,events做不同处理
// 在这里注入针对不同属性的处理函数
const patch = createPatchFunction([
 _class, // makes it easy to toggle classes
 props,
 style,
 attrs,
 events
])

// => createPatchFunction返回patch函数,patch函数通过对比虚拟节点的差异,对节点进行增删更新
// 最后调用原生的dom api更新html
return function patch (oldVnode, vnode) {
 var i, elm, parent
 var insertedVnodeQueue = []
 // pre hook
 for (i = 0; i < cbs.pre.length; ++i) cbs.pre[i]()

 if (isUndef(oldVnode.sel)) {
  oldVnode = emptyNodeAt(oldVnode)
 }

 if (sameVnode(oldVnode, vnode)) {
  // someNode can patch
  patchVnode(oldVnode, vnode, insertedVnodeQueue)
 } else {
  // 正常的不复用 remove insert
  elm = oldVnode.elm
  parent = api.parentNode(elm)

  createElm(vnode, insertedVnodeQueue)

  if (parent !== null) {
   api.insertBefore(parent, vnode.elm, api.nextSibling(elm))
   removeVnodes(parent, [oldVnode], 0, 0)
  }
 }

 for (i = 0; i < insertedVnodeQueue.length; ++i) {
  insertedVnodeQueue[i].data.hook.insert(insertedVnodeQueue[i])
 }

 // hook post
 for (i = 0; i < cbs.post.length; ++i) cbs.post[i]()
 return vnode
}

结尾

以上分析了vue从template 到节点渲染的大致实现,当然也有某些地方没有全面分析的地方,其中template解析为ast主要通过正则匹配实现,及节点渲染及更新的patch过程主要通过节点操作对比来实现。但是我们对编译template字符串 => 代理data数据/methods的this绑定 => 数据观察 => 建立watch及更新渲染的大致流程有了个比较完整的认知。

到此这篇关于浅谈vue的第一个commit分析的文章就介绍到这了,更多相关vue commit内容请搜索三水点靠木以前的文章或继续浏览下面的相关文章希望大家以后多多支持三水点靠木!

Javascript 相关文章推荐
JS 获取浏览器和屏幕宽高等信息的实现思路及代码
Jul 31 Javascript
jquery弹窗插件colorbox绑定动态生成元素的方法
Jun 20 Javascript
javasript实现密码的隐藏与显示
May 08 Javascript
对JavaScript客户端应用编程的一些建议
Jun 24 Javascript
jquery validate表单验证的基本用法入门
Jan 18 Javascript
JQuery EasyUI的使用
Feb 24 Javascript
JavaScript 轮播图和自定义滚动条配合鼠标滚轮分享代码贴
Oct 28 Javascript
Echarts基本用法_动力节点Java学院整理
Aug 11 Javascript
JavaScript引用类型Object常见用法实例分析
Aug 08 Javascript
JS module的导出和导入的实现代码
Feb 25 Javascript
React中使用UMEditor的方法示例
Dec 27 Javascript
Angular 多模块项目构建过程
Feb 13 Javascript
从零开始在vue-cli4配置自适应vw布局的实现
Jun 08 #Javascript
详解Vue Cli浏览器兼容性实践
Jun 08 #Javascript
解决微信授权成功后点击按返回键出现空白页和报错的问题
Jun 08 #Javascript
微信h5静默和非静默授权获取用户openId的方法和步骤
Jun 08 #Javascript
基于javascript处理二进制图片流过程详解
Jun 08 #Javascript
vue-router的hooks用法详解
Jun 08 #Javascript
Vue自定义render统一项目组弹框功能
Jun 07 #Javascript
You might like
header()函数使用说明
2006/11/23 PHP
PHP文件与目录操作示例
2016/12/24 PHP
php+redis实现商城秒杀功能
2020/11/19 PHP
PHP通过curl获取接口URL的数据方法
2018/05/31 PHP
javascript 写类方式之八
2009/07/05 Javascript
仿新浪微博返回顶部的jquery实现代码
2012/10/01 Javascript
js作用域及作用域链概念理解及使用
2013/04/15 Javascript
js 通用订单代码
2013/12/23 Javascript
JS调用页面表格导出excel示例代码
2014/03/18 Javascript
使用纯javascript实现经典扫雷游戏
2015/04/23 Javascript
JS+CSS实现仿触屏手机拨号盘界面及功能模拟完整实例
2015/05/16 Javascript
jQuery获取父元素节点、子元素节点及兄弟元素节点的方法
2016/04/14 Javascript
jQuery遍历json的方法分析
2016/04/16 Javascript
JavaScript实现页面定时刷新(定时器,meta)
2016/10/12 Javascript
Bootstrap基本样式学习笔记之表格(2)
2016/12/07 Javascript
微信小程序 本地数据读取实例
2017/04/27 Javascript
web前端开发中常见的多列布局解决方案整理(一定要看)
2017/10/15 Javascript
Vue组件的使用教程详解
2018/01/05 Javascript
详解tween.js 中文使用指南
2018/01/05 Javascript
Vue.js中的computed工作原理
2018/03/22 Javascript
JavaScript数据结构与算法之二叉树实现查找最小值、最大值、给定值算法示例
2019/03/01 Javascript
TypeScript中的方法重载详解
2019/04/12 Javascript
JS实现音乐钢琴特效
2020/01/06 Javascript
[05:39]2014DOTA2西雅图国际邀请赛 淘汰赛7月14日TOPPLAY
2014/07/14 DOTA
[02:43]2018DOTA2亚洲邀请赛主赛事首日TOP5
2018/04/04 DOTA
python中利用队列asyncio.Queue进行通讯详解
2017/09/10 Python
pandas DataFrame 行列索引及值的获取的方法
2019/07/02 Python
python+jinja2实现接口数据批量生成工具
2019/08/28 Python
python用tkinter实现一个gui的翻译工具
2020/10/26 Python
医药类个人求职的自我评价
2014/02/12 职场文书
出国英文推荐信
2014/05/10 职场文书
2014高考励志标语
2014/06/05 职场文书
2015企业年终工作总结范文
2015/05/27 职场文书
python开发的自动化运维工具ansible详解
2021/08/07 Python
mysql创建存储过程及函数详解
2021/12/04 MySQL
win10电脑右下角输入法图标不见了?Win10右下角不显示输入法的解决方法
2022/07/23 数码科技