浅谈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 相关文章推荐
自适应图片大小的弹出窗口
Jul 27 Javascript
IE8 原生JSON支持
Apr 13 Javascript
js检查页面上有无重复id的实现代码
Jul 17 Javascript
javascript获取设置div的高度和宽度兼容任何浏览器
Sep 22 Javascript
用html5 js实现点击一个按钮达到浏览器全屏效果
May 28 Javascript
Jquery动态替换div内容及动态展示的方法
Jan 23 Javascript
JS操作HTML自定义属性的方法
Feb 10 Javascript
JavaScript使用Max函数返回两个数字中较大数的方法
Apr 06 Javascript
JSON字符串和对象之间的转换详解
May 26 Javascript
JavaScript中使用sencha gridpanel 编辑单元格、改变单元格颜色
Nov 26 Javascript
javascript瀑布流式图片懒加载实例解析与优化
Feb 23 Javascript
手写简单的jQuery雪花飘落效果实例
Apr 22 jQuery
从零开始在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下清空字符串中的HTML标签的代码
2010/09/06 PHP
基于php中使用excel的简单介绍
2013/08/02 PHP
php对文件夹进行相关操作(遍历、计算大小)
2015/11/04 PHP
PHP实现的服务器一致性hash分布算法示例
2018/08/09 PHP
div移动 输入框不能输入的问题
2009/11/19 Javascript
Javascript 面向对象 继承
2010/05/13 Javascript
用jquery与css打造个性化的单选框和复选框
2010/10/20 Javascript
js取值中form.all和不加all的区别介绍
2014/01/20 Javascript
jquery实现上下左右滑动的方法
2015/02/09 Javascript
loading动画特效小结
2017/01/22 Javascript
javascript图片预览和上传(兼容IE)
2017/03/15 Javascript
js实现省市级联效果分享
2017/08/10 Javascript
JavaScript实现HTML5游戏断线自动重连的方法
2017/09/18 Javascript
茶余饭后聊聊Vue3.0响应式数据那些事儿
2019/10/30 Javascript
Node.js API详解之 net模块实例分析
2020/05/18 Javascript
[48:48]2014 DOTA2国际邀请赛中国区预选赛 SPD-GAMING VS Dream TIME
2014/05/21 DOTA
php使用递归与迭代实现快速排序示例
2014/01/23 Python
用python登录Dr.com思路以及代码分享
2014/06/25 Python
python @property的用法及含义全面解析
2018/02/01 Python
python使用Pycharm创建一个Django项目
2018/03/05 Python
让你Python到很爽的加速递归函数的装饰器
2019/05/26 Python
Python依赖包整体迁移方法详解
2019/08/15 Python
整理HTML5的一些新特性与Canvas的常用属性
2016/01/29 HTML / CSS
森海塞尔美国官网:Sennheiser耳机与耳麦
2017/07/19 全球购物
英国女性时尚精品店:THE DRESSING ROOM
2018/05/23 全球购物
商务日语毕业生自荐信范文
2013/11/14 职场文书
小区停车场管理制度
2014/01/27 职场文书
工商干部先进事迹
2014/05/14 职场文书
授权委托书(公民个人适用)
2014/09/19 职场文书
贪污检举信范文
2015/03/02 职场文书
2015年市场部工作总结
2015/04/30 职场文书
《1942》观后感
2015/06/08 职场文书
高中美术教学反思
2016/02/17 职场文书
学校2016年圣诞节活动总结
2016/03/31 职场文书
2016年庆“七一”主题党日活动总结
2016/04/05 职场文书
Spring Boot DevTools 全局配置学习指南
2022/03/31 Java/Android