浅谈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 相关文章推荐
基于jquery可配置循环左右滚动例子
Sep 09 Javascript
js中document.getElementByid、document.all和document.layers区分介绍
Dec 08 Javascript
js replace替换所有匹配的字符串
Feb 13 Javascript
jQuery实现表单提交时判断的方法
Dec 13 Javascript
jQuery插件实现多级联动菜单效果
Dec 01 Javascript
jquery在ie7下选择器的问题导致append失效的解决方法
Jan 10 Javascript
Node.js实现数据推送
Apr 14 Javascript
简单谈谈vue的过渡动画(推荐)
Oct 11 Javascript
vue中子组件向父组件传递数据的实例代码(实现加减功能)
Apr 20 Javascript
vue-cli3脚手架的配置及使用教程
Aug 28 Javascript
JS跨域请求的问题解析
Dec 03 Javascript
react实现同页面三级跳转路由布局
Sep 26 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
提升PHP执行速度全攻略(下)
2006/10/09 PHP
PHP实现过滤各种HTML标签
2015/05/17 PHP
PHP和C#可共用的可逆加密算法详解
2015/10/26 PHP
php命名空间设计思想、用法与缺点分析
2019/07/17 PHP
php数值计算num类简单操作示例
2020/05/15 PHP
js 获取时间间隔实现代码
2014/05/12 Javascript
jQuery实现html元素拖拽
2015/07/21 Javascript
你有必要知道的25个JavaScript面试题
2015/12/29 Javascript
Bootstrap实现圆角、圆形头像和响应式图片
2016/12/14 Javascript
使用JS 插件qrcode.js生成二维码功能
2017/02/20 Javascript
AngularJS 单选框及多选框的双向动态绑定
2017/04/20 Javascript
浅谈jQuery框架Ajax常用选项
2017/07/08 jQuery
JavaScript事件委托原理与用法实例分析
2018/06/07 Javascript
Layui 设置select下拉框自动选中某项的方法
2018/08/14 Javascript
VUE-cli3使用 svg-sprite-loader
2018/10/20 Javascript
vue项目中mock.js的使用及基本用法
2019/05/22 Javascript
Vue项目实现简单的权限控制管理功能
2019/07/17 Javascript
微信小程序实现加入购物车滑动轨迹
2020/11/18 Javascript
python的tkinter布局之简单的聊天窗口实现方法
2014/09/03 Python
python简单文本处理的方法
2015/07/10 Python
Python编程求解二叉树中和为某一值的路径代码示例
2018/01/04 Python
python互斥锁、加锁、同步机制、异步通信知识总结
2018/02/11 Python
Python读取excel指定列生成指定sql脚本的方法
2018/11/28 Python
浅谈Keras的Sequential与PyTorch的Sequential的区别
2020/06/17 Python
H5页面适配iPhoneX(就是那么简单)
2019/12/02 HTML / CSS
Emporio Armani腕表天猫官方旗舰店:乔治·阿玛尼为年轻人设计的副线品牌
2017/07/02 全球购物
Europcar德国:全球汽车租赁领域的领导者
2018/08/15 全球购物
一名女生的自荐信
2013/12/08 职场文书
顶岗实习接收函
2014/01/09 职场文书
《彭德怀和他的大黑骡子》教学反思
2014/04/12 职场文书
临床医学生职业规划书范文
2014/10/25 职场文书
保险内勤岗位职责
2015/04/13 职场文书
商场圣诞节活动总结
2015/05/06 职场文书
公司宣传语大全
2015/07/13 职场文书
2016廉洁从业学习心得体会
2016/01/19 职场文书
幼儿园语言教学反思
2016/02/23 职场文书