48°

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) =&gt; {
  // key =&gt; 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 &lt; l; i++) {
        e = value[i]
        e &amp;&amp; e.__ob__ &amp;&amp; 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.prei

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.posti return vnode }

结尾

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


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

本文转载自博客园,原文链接:https://www.cnblogs.com/formercoding/p/13045574.html

全部评论: 0

    我有话说: