zoukankan      html  css  js  c++  java
  • vue的第一个commit分析

    为什么写这篇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)),从elhtml字符串到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及更新渲染的大致流程有了个比较完整的认知。


    欢迎到前端学习打卡群一起学习~516913974

  • 相关阅读:
    Viusal Studio 2022 正式版安装秘钥
    关于云计算,云存储,和自己开发的云存储的小工具
    网盘工具比较,以及自己开发的网盘工具
    VARIANT及相关类
    关于 BSTR, CComBSTR and _bstr_t
    如何真正发挥Google Docs的威力
    ORM框架EntitysCodeGenerate自定义分页查询及快捷执行SQL(CreateSQL)示例
    关于Java Servlet的中文乱码
    ORM框架VB/C#.Net实体代码生成工具(EntitysCodeGenerate) 【ECG】4.3 介绍
    通用JS验证框架(ChkInputs)概述
  • 原文地址:https://www.cnblogs.com/formercoding/p/13045574.html
Copyright © 2011-2022 走看看