浅谈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)),从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的实现原理:
- 建立观察者观察data数据的更改 (new observe)
- 在编译的时候,当某个代码片段或节点依赖data数据,为该节点建议订阅者,订阅data中某些数据的更新(new watch)
- 当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内容请搜索以前的文章或继续浏览下面的相关文章希望大家以后多多支持!