使用rollup搭建开发环境 使用rollup打包第三方库会比webpack更轻量,速度更快
首先安装依赖
1 2 3 npm init -y npm install rollup rollup-plugin-babel @babel/core @babel/preset-env --save-dev
然后添加 rollup 的配置文件 rollup.config.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 import babel from "rollup-plugin-babel" export default { input:"./src/index.js" , output:{ file:"./desc/vue.js" , name:"Vue" , format:"umd" , sourcemap:true , }, plugins:[ babel({ exclude:"node_modules/**" , }) ] }
添加 babel 的配置文件 .babelrc
1 2 3 4 5 { "presets" : [ "@babel/preset-env" ] }
修改 package.json
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 { "name" : "vue2" , "version" : "1.0.0" , "description" : "" , "main" : "index.js" , "scripts" : { "dev" : "rollup -cw" }, "keywords" : [], "author" : "" , "license" : "ISC" , "devDependencies" : { "@babel/core" : "^7.23.2" , "@babel/preset-env" : "^7.23.2" , "rollup" : "^4.3.0" , "rollup-plugin-babel" : "^4.4.0" }, "type" : "module" }
记得在 package.json
后面添加 "type": "module"
,否则启动时会提示 import babel from "rollup-plugin-babel"
错误
准备完成后运行启动命令
出现上图表示启动成功,并且正在监听文件变化,文件变化后会自动重新打包
查看打包出来的文件
然后新建一个 index.html
引入打包出来的 vue.js
1 2 3 4 5 6 7 8 9 10 11 12 13 <!DOCTYPE html > <html lang ="en" > <head > <meta charset ="UTF-8" > <title > Title</title > <script src ="./vue.js" > </script > </head > <body > <script > console .log(Vue) </script > </body > </html >
访问这个文件,并打开控制台查看打印
至此,我们准备工作完成,接下来开始实现Vue核心部分。
初始化数据 修改 src/index.js
1 2 3 4 5 6 7 8 9 import {initMixin} from "./init" ;function Vue (options ) { this ._init(options) } initMixin(Vue) export default Vue
添加 init.js
,用于初始化数据操作,并导出 initMixin 方法
1 2 3 4 5 6 7 8 9 10 11 import {initStatus} from "./state.js" ;export function initMixin (Vue ) { Vue.prototype._init = function (options ) { const vm = this vm.$options = options initStatus(vm) } }
state.js
的写法
1 2 3 4 5 6 7 8 9 10 11 12 export function initStatus (vm ) { const opt = vm.$options if (opt.data){ initData(vm) } } function initData (vm ) { let data = vm.$options.data data = typeof data === "function" ? data.call(vm) : data console .log(data) }
我们打开控制台查看打印的东西
可以发现已经正确的得到data数据
实现对象的响应式 现在我们在 initData 方法中拿到了data,接下来就是对data中的属性进行数据劫持
在 initData 中添加 observe 方法,并传递data对象
1 2 3 4 5 6 function initData (vm ) { let data = vm.$options.data data = typeof data === "function" ? data.call(vm) : data observe(data) }
新建 observe/index.js
文件,实现 observe 方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 class Observer { constructor (data ) { this .walk(data) } walk (data ) { Object .keys(data).forEach(key => { defineReactive(data,key,data[key]) }) } } export function defineReactive (target,key,value ) { observe(value) Object .defineProperty(target,key,{ get ( ) { return value }, set (newValue ) { if (newValue === value) return value = newValue } }) } export function observe (data ) { if (typeof data !== "object" || data === null ) return return new Observer(data) }
现在对数据就劫持完成了,但是我们如何获取呢?我们可以吧data方法返回的对象挂载到Vue的实例上即可
还是在 initData 方法内添加代码,并且增加一个 proxy 方法,让我们可以通过 vm.xxx 的方式直接获取data中的属性值
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 function initData (vm ) { let data = vm.$options.data data = typeof data === "function" ? data.call(vm) : data observe(data) vm._data = data proxy(vm,"_data" ) } function proxy (target,key ) { for (const dataKey in target[key]) { Object .defineProperty(target, dataKey,{ get ( ) { return target[key][dataKey] }, set (newValue ) { target[key][dataKey] = newValue } }) } }
现在来打印一下 vm
通过打印发现,vm 自身上就有了data中定义的属性
并且直接通过 vm 来读取和设置属性值也是可以的
实现数组的响应式 实现思路:
首先遍历数组中的内容,吧数组中的数据变成响应式的
如果调用的数组中的方法,添加了新的数据,则也要吧新的数据变成响应式的,这里可以劫持7个变异方法来实现
首先在 Observer 类中添加判断,如果data是一个数组,则单独走一个observeArray方法,来实现对数组的响应式处理
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 import {newArrayProperty} from "./array.js" ;class Observer { constructor (data ) { Object .defineProperty(data,"__ob__" ,{ value:this , enumerable:false }) if (Array .isArray(data)){ data.__proto__ = newArrayProperty this .observeArray(data) }else { this .walk(data) } } walk (data ) { Object .keys(data).forEach(key => { defineReactive(data,key,data[key]) }) } observeArray (data ) { data.forEach(item => observe(item)) } }
这里在 data 中定义了 __ob__
属性,并且值等于当前的 Observer 实例,是为了在 array.js 中拿到 Observe 实例中的 observeArray 方法,来实现对新传递进来的数据进行响应式处理
既然有了这个 __ob__
属性,我们就可以判断一下,如果 data 中有了 __ob__
属性,则表示这个数据已经被响应式了,则不需要进行再次响应式,所以我们可以在 observe 方法中加一个判断
1 2 3 4 5 6 7 8 9 export function observe (data ) { if (typeof data !== "object" || data === null ) return if (data.__ob__){ return data.__ob__ } return new Observer(data) }
然后下面是 array.js 的实现代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 let oldArrayProperty = Array .prototypeexport let newArrayProperty = Object .create(oldArrayProperty)let methods = ["push" , "pop" , "unshift" , "shift" , "reserve" , "sort" , "splice" ]methods.forEach(method => { newArrayProperty[method] = function (...args ) { let inserted; let ob = this .__ob__ switch (method) { case "push" : case "unshift" : case "shift" : inserted = args; break case "splice" : inserted = args.slice(2 ) break default : break ; } if (inserted){ ob.observeArray(inserted) } return oldArrayProperty[method].call(this , ...args); } })
现在看一下效果
可以看到我们新 push 的数据也被响应式了
解析HTML模板 我们可以根据option中的el来获取到根标签,然后获取对应的html,拿到html后开始解析
先写一些测试代码,准备一个html页面
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 <!DOCTYPE html > <html lang ="en" > <head > <meta charset ="UTF-8" > <title > Title</title > <script src ="./vue.js" > </script > </head > <body > <div id ="app" > <div style ="font-size: 15px;color: blue" data-name = "123" > {{name}} </div > <div style ="color: red" > {{age}} </div > </div > </body > <script > const vm = new Vue({ el:"#app" , data ( ) { return { name:"szx" , age:18, address:{ price:100, name:"少林寺" }, hobby:['each' ,'write' ,{a :"tome" }] } } }) </script > </html >
然后来到 init.js 中的 initMixin 方法,判断一下是否有 el 这个属性,如果有则开始进行模板解析
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 import {initStatus} from "./state.js" ;import {compilerToFunction} from "./compile/index.js" ;export function initMixin (Vue ) { Vue.prototype._init = function (options ) { const vm = this vm.$options = options initStatus(vm) if (vm.$options.el){ vm.$mount(vm.$options.el) } } Vue.prototype.$mount = function (el ) { let template; const vm = this const opts = vm.$options if (!opts.render){ if (!opts.template && opts.el){ template = document .querySelector(el).outerHTML } if (opts.template){ template = opts.template } if (template){ const render = compilerToFunction(template) } } } }
下面就是 compilerToFunction
的代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 const unicodeRegExp = /a-zA-Z\u00B7\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u037D\u037F-\u1FFF\u200C-\u200D\u203F-\u2040\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD/ const ncname = `[a-zA-Z_][\\-\\.0-9_a-zA-Z${unicodeRegExp.source} ]*` const qnameCapture = `((?:${ncname} \\:)?${ncname} )` const startTagOpen = new RegExp (`^<${qnameCapture} ` )const endTag = new RegExp (`^<\\/${qnameCapture} [^>]*>` )const attribute = /^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/ const startTagClose = /^\s*(\/?)>/ const defaultTagRE = /\{\{((?:.|\r?\n)+?)\}\}/g function parseHtml (html ) { function advance (n ) { html = html.substring(n) } function parseStart ( ) { let start = html.match(startTagOpen) if (start){ const match = { tagName:start[1 ], attrs:[] } advance(start[0 ].length) let attr,end; while (!(end = html.match(startTagClose)) && (attr = html.match(attribute))){ match.attrs.push({ name:attr[1 ], value:attr[3 ] || attr[4 ] || attr[5 ] || true }) advance(attr[0 ].length) } if (end){ advance(end[0 ].length) } console .log(match) return match } return false } while (html){ let textEnd = html.indexOf('<' ); if (textEnd === 0 ){ const startTagMatch = parseStart() if (startTagMatch){ continue } let endTagMatch = html.match(endTag) if (endTagMatch){ advance(endTagMatch[0 ].length) continue } } if (textEnd > 0 ){ let text = html.substring(0 ,textEnd) if (text){ advance(text.length) } } } console .log(html) return "" } export function compilerToFunction (template ) { let ast = parseHtml(template) return "" }
这段代码在不断的解析html内容,匹配到开始标签,就会标签名称和属性放在match数组中,并且删除一已经匹配到的内容,如果匹配到文本或者结束版本则删除匹配到的内容,最终html变成空,表示解析过程就结束了。
我们通过打印看一下html被解析的过程
可以看到html的内容再不断减少
接下来,我们只需要在这些方法中添加如果匹配到开始标签,就触发一个方法处理开始标签的内容,如果匹配到文本,就处理文本内容,如果匹配到结束标签,就处理结束标签的内容。
在 parseHtml 方法中添加三个方法如下,分别处理开始标签,文本,结束标签
onStartTag
onText
onCloseTag
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 const unicodeRegExp = /a-zA-Z\u00B7\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u037D\u037F-\u1FFF\u200C-\u200D\u203F-\u2040\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD/ const ncname = `[a-zA-Z_][\\-\\.0-9_a-zA-Z${unicodeRegExp.source} ]*` const qnameCapture = `((?:${ncname} \\:)?${ncname} )` const startTagOpen = new RegExp (`^<${qnameCapture} ` )const endTag = new RegExp (`^<\\/${qnameCapture} [^>]*>` )const attribute = /^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/ const startTagClose = /^\s*(\/?)>/ const defaultTagRE = /\{\{((?:.|\r?\n)+?)\}\}/g function parseHtml (html ) { function onStartTag (tag,attrs ) { console .log(tag,attrs) console .log("开始标签" ) } function onText (text ) { console .log(text) console .log("文本" ) } function onCloseTag (tag ) { console .log(tag) console .log("结束标签" ) } function advance (n ) { html = html.substring(n) } function parseStart ( ) { let start = html.match(startTagOpen) if (start){ const match = { tagName:start[1 ], attrs:[] } advance(start[0 ].length) let attr,end; while (!(end = html.match(startTagClose)) && (attr = html.match(attribute))){ match.attrs.push({ name:attr[1 ], value:attr[3 ] || attr[4 ] || attr[5 ] || true }) advance(attr[0 ].length) } if (end){ advance(end[0 ].length) } return match } return false } while (html){ let textEnd = html.indexOf('<' ); if (textEnd === 0 ){ const startTagMatch = parseStart() if (startTagMatch){ onStartTag(startTagMatch.tagName,startTagMatch.attrs) continue } let endTagMatch = html.match(endTag) if (endTagMatch){ onCloseTag(endTagMatch[1 ]) advance(endTagMatch[0 ].length) continue } } if (textEnd > 0 ){ let text = html.substring(0 ,textEnd) if (text){ onText(text) advance(text.length) } } } console .log(html) return "" } export function compilerToFunction (template ) { let ast = parseHtml(template) return "" }
并在在相对应的代码中调用者三个方法
查看打印效果
接下来构建语法树
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 function parseHtml (html ) { const ELEMENT_TYPE = 1 const TEXT_TYPE = 3 const stack = [] let currentParent; let root; function createNode (tag,attrs ) { return { tag, attrs, type:ELEMENT_TYPE, children:[], parent:null } } function onStartTag (tag,attrs ) { let node = createNode(tag,attrs) if (!root){ root = node } if (currentParent){ node.parent = currentParent currentParent.children.push(node) } stack.push(node) currentParent = node } function onText (text ) { text = text.replace(/\s/g ,"" ) text && currentParent.children.push({ type:TEXT_TYPE, text, parent:currentParent }) } function onCloseTag (tag ) { stack.pop() currentParent = stack[stack.length -1 ] } return root }
然后打印一下生成的ast语法树
1 2 3 4 5 export function compilerToFunction (template ) { let ast = parseHtml(template) console .log(ast) return "" }
代码生成的实现原理 现在我们已经得到了AST语法树,接下来我们就需要根据得到的AST语法树转化成一段cvs字符串
_c 表示创建元素
_v 表示处理文本内容
_s 表示处理花括号包裹的文本
这里提前吧 parseHtml 方法抽离出来放在 parse.js 文件中并导出
然后在 compilerToFunction 方法中添加 codegen 方法,根据 ast 语法树生成字符串代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 import {parseHtml,ELEMENT_TYPE} from "./parse.js" ;function genProps (attrs ) { let str = `` attrs.forEach(attr => { if (attr.name === "style" ){ let obj = {} attr.value.split(";" ).forEach(sItem => { let [key,value] = sItem.split(":" ) obj[key] = value.trim() }) attr.value = obj } str += `${JSON .stringify(attr.name)} :${JSON .stringify(attr.value)} ,` }) str = `{${str.slice(0 ,-1 )} }` return str } function genChildren (children ) { return children.map(child => gen(child)).join("," ) } const defaultTagRE = /\{\{((?:.|\r?\n)+?)\}\}/g ;function gen (child ) { if (child.type === ELEMENT_TYPE) { return codegen(child) } else { let text = child.text if (!defaultTagRE.test(text)) { return `_v(${JSON .stringify(text)} )` } else { let token = [] let match; let lastIndex = 0 defaultTagRE.lastIndex = 0 while (match = defaultTagRE.exec(text)) { let index = match.index if (index > lastIndex) { token.push(JSON .stringify(text.slice(lastIndex, index))) } token.push(`_s(${match[1 ].trim()} )` ) lastIndex = index + match[0 ].length } if (lastIndex < text.length) { token.push(JSON .stringify(text.slice(lastIndex))) } return `_v(${token.join("+" )} )` } } } function codegen (ast ) { let code = `_c( ${JSON .stringify(ast.tag)} , ${ast.attrs.length ? genProps(ast.attrs) : "null" } , ${ast.children.length ? genChildren(ast.children) : "null" } )` return code } export function compilerToFunction (template ) { let ast = parseHtml(template) let cvs = codegen(ast) console .log(cvs) return "" }
效果如下图
生成render函数 现在我们得到了一个字符串,并不是一个函数,下面就是要把这个字符串变成一个render函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 export function compilerToFunction (template ) { let ast = parseHtml(template) let code = codegen(ast) code = `with(this){return ${code} }` let render = new Function (code) console .log(render.toString()) return render }
这里的 with 方法可以自动从 this 中读取变量值
1 2 3 4 5 6 7 8 9 10 11 with (object) { }
简单示例
1 2 3 4 5 6 7 let testObj = { name:"Tome" , age:18 } with (testObj) { console .log(name + age); }
现在有了render函数后,将render 返回并添加到 $options 中
在 initMixin 方法中添加
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 import {initStatus} from "./state.js" ;import {compilerToFunction} from "./compile/index.js" ;import {mountComponent} from "./lifecycle.js" ;export function initMixin (Vue ) { Vue.prototype._init = function (options ) { const vm = this vm.$options = options initStatus(vm) if (vm.$options.el){ vm.$mount(vm.$options.el) } } Vue.prototype.$mount = function (el ) { let template; const vm = this const opts = vm.$options if (!opts.render){ if (!opts.template && opts.el){ template = document .querySelector(el).outerHTML } if (opts.template){ template = opts.template } if (template){ opts.render = compilerToFunction(template) } } mountComponent(vm,el) } }
添加 mountComponent 方法,新建一个文件,单独写个这个方法
lifecycle.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 export function initLifeCycle (Vue ) { Vue.prototype._render = function ( ) { } Vue.prototype._update = function ( ) { } } export function mountComponent (vm,el ) { vm.$el = el vm._update(vm._render()) }
initLifeCycle 方法需要接收一个 Vue,我们可以在 index.js 文件中添加调用
1 2 3 4 5 6 7 8 9 10 11 import {initMixin} from "./init" ;import {initLifeCycle} from "./lifecycle.js" ;function Vue (options ) { this ._init(options) } initMixin(Vue) initLifeCycle(Vue) export default Vue
创建虚拟节点并更新视图 完善 lifecycle.js 文件的代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 import {createElementVNode, createTextVNode} from "./vdom/index.js" ;export function initLifeCycle (Vue ) { Vue.prototype._c = function ( ) { return createElementVNode(this ,...arguments) } Vue.prototype._v = function ( ) { return createTextVNode(this ,...arguments) } Vue.prototype._s = function (value ) { return value } Vue.prototype._render = function ( ) { return this .$options.render.call(this ) } Vue.prototype._update = function (vnode ) { const elm = document .querySelector(this .$options.el) patch(elm,vnode) } } function patch (oldVNode,newVNode ) { const isRealEle = oldVNode.nodeType; if (isRealEle){ const elm = oldVNode const parentElm = elm.parentNode let newRealEl = createEle(newVNode) parentElm.insertBefore(newRealEl,elm.nextSibling) parentElm.removeChild(elm) } } function createEle (vnode ) { let {tag,data,children,text} = vnode if (typeof tag === "string" ){ vnode.el = document .createElement(tag) Object .keys(data).forEach(prop => { if (prop === "style" ){ Object .keys(data.style).forEach(sty => { vnode.el.style[sty] = data.style[sty] }) }else { vnode.el.setAttribute(prop,data[prop]) } }) children.forEach(child => { vnode.el.appendChild(createEle(child)) }) }else { vnode.el = document .createTextNode(text) } return vnode.el } export function mountComponent (vm,el ) { vm.$el = el vm._update(vm._render()) }
vnode/index.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 export function createElementVNode (vm,tag,prop,...children ) { if (!prop){ prop = {} } let key = prop.key if (key){ delete prop.key } return vnode(vm,tag,prop,key,children,undefined ) } export function createTextVNode (vm,text ) { return vnode(vm,undefined ,undefined ,undefined ,undefined ,text) } function vnode (vm,tag,data,key,children,text ) { children = children.filter(Boolean ) return { vm, tag, data, key, children, text } }
此时我们的页面就可以正常显示数据了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 <!DOCTYPE html > <html lang ="en" > <head > <meta charset ="UTF-8" > <title > Title</title > <script src ="./vue.js" > </script > </head > <body > <div id ="app" style ="color: red;font-size: 20px" > <div style ="font-size: 15px;color: blue" data-name = "123" > 你好 {{ name }} hello {{ age }} word </div > <span > {{ address.name }} </span > </div > </body > <script > const vm = new Vue({ el:"#app" , data ( ) { return { name:"szx" , age:18, address:{ price:100, name:"少林寺" }, hobby:['each' ,'write' ,{a :"tome" }] } } }) </script > </html >
实现依赖收集 现在初次渲染已经可以吧页面上绑定的数据渲染成我们定义的数据,但是当我们改变data数据时,页面不会发生更新,这里就要使用观察者模式,实现依赖收集。
读取某个属性时,会调用get方法,在get方法中收集watcher(观察者),然后当更新数据时,会调用set方法,通知当前这个属性绑定的观察者去完成更新视图的操作。
在get方法中收集watcher的同时,watcher也要收集这个属性(dept),要知道我当前的这个watcher下面有几个dept。
一个dept对应多个watcher,因为一个属性可能会在多个视图中使用
一个watcher对应多个dept,因为一个组件中会有多个属性
下面的代码实现逻辑
修改 lifecycle.js
文件中的 mountComponent
方法
1 2 3 4 5 6 7 8 9 10 11 12 13 export function mountComponent (vm,el ) { vm.$el = el const updateComponent = ()=> { vm._update(vm._render()) } new Watcher(vm,updateComponent) }
新建 observe/watcher.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 import Dep from "./dep.js" ;let id = 0 class Watcher { constructor (vm,fn ) { this .id = id++ this .getter = fn this .depts = [] this .deptSet = new Set () this .get() } get ( ) { Dep.target = this this .getter() Dep.target = null } addDep (dep ) { if (!this .deptSet.has(dep.id)){ this .depts.push(dep) this .deptSet.add(dep.id) dep.addSubs(this ) } } update ( ) { this .get() } } export default Watcher
对应的 observe/dep.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 let id = 0 class Dep { constructor ( ) { this .id = id++ this .subs = [] } depend ( ) { Dep.target.addDep(this ) } addSubs (watcher ) { this .subs.push(watcher) } notify ( ) { this .subs.forEach(watcher => watcher.update()) } } export default Dep
dep 就是被观察者,watcher 就是观察者,在属性的 get 和 set 方法中进行依赖收集和更新通知
修改 observe/index.js
的 defineReactive
方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 export function defineReactive (target,key,value ) { observe(value) let dep = new Dep(); Object .defineProperty(target,key,{ get ( ) { if (Dep.target){ dep.depend() } return value }, set (newValue ) { if (newValue === value) return value = newValue dep.notify() } }) }
测试视图更新
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 const vm = new Vue({ el:"#app" , data ( ) { return { name:"szx" , age:18 , address:{ price:100 , name:"少林寺" }, hobby:['each' ,'write' ,{a :"tome" }] } } }) function addAge ( ) { vm.name = "李四" vm.age = 20 }
我们发现,点击更新按钮后视图确实发生了更新。但是控制台打印了两次更新。这是因为我们在 addAge 方法中对两个属性进行了更改,所以触发了两次更新。下面我们来解决这个问题,让他只触发一次更新
实现异步更新 修改 Watch 中的 update 方法,同时新增一个 run 方法,专门用于更新视图操作
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 import Dep from "./dep.js" ;let id = 0 class Watcher { constructor (vm,fn ) { this .id = id++ this .getter = fn this .depts = [] this .deptSet = new Set () this .get() } get ( ) { Dep.target = this this .getter() Dep.target = null } addDep (dep ) { if (!this .deptSet.has(dep.id)){ this .depts.push(dep) this .deptSet.add(dep.id) dep.addSubs(this ) } } update ( ) { queueWatcher(this ) } run ( ) { console .log('更新视图' ) this .getter() } } let queue = []let watchObj = {}let padding = false function queueWatcher (watcher ) { if (!watchObj[watcher.id]){ watchObj[watcher.id] = true queue.push(watcher) if (!padding){ nextTick(flushSchedulerQueue,0 ) padding = true } } } function flushSchedulerQueue ( ) { let flushQueue = queue.slice(0 ) queue = [] watchObj = {} padding = false flushQueue.forEach(cb => cb.run()) } let callbacks = []let waiting = false export function nextTick (cb ) { callbacks.push(cb) if (!waiting){ Promise .resolve().then(flushCallback) waiting = true } } function flushCallback ( ) { let cbs = callbacks.slice(0 ) callbacks = [] waiting = false cbs.forEach(cb => cb()) } export default Watcher
在 src/index.js
中挂载全局的 $nexitTick 方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 import {initMixin} from "./init" ;import {initLifeCycle} from "./lifecycle.js" ;import {nextTick} from "./observe/watcher.js" ;function Vue (options ) { this ._init(options) } Vue.prototype.$nextTick = nextTick initMixin(Vue) initLifeCycle(Vue) export default Vue
页面使用
1 2 3 4 5 6 7 function addAge ( ) { vm.name = "李四" vm.age = 20 vm.$nextTick(()=> { console .log(document .querySelector("#name" ).innerText) }) }
点击更新按钮执行 addAge 方法,可以在控制台看到只触发了一个更新视图,并且获取的页面也是更新后的
实现mixin核心功能 mixin的核心是合并对象,将Vue.mixin中的对象和在Vue中定义的属性进行合并,然后再初始化状态前后调用不同的Hook即可
首先在 index.js 中添加方法调用
index.js
文件增加 initGlobalApi,传入 Vue
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 import {initMixin} from "./init"; import {initLifeCycle} from "./lifecycle.js"; import {nextTick} from "./observe/watcher.js"; + import {initGlobalApi} from "./globalApi.js"; function Vue(options){ this._init(options) } Vue.prototype.$nextTick = nextTick initMixin(Vue) initLifeCycle(Vue) + initGlobalApi(Vue) export default Vue
globalApi.js
内容如下
1 2 3 4 5 6 7 8 9 10 import {mergeOptions} from "./utils.js" ;export function initGlobalApi (Vue ) { Vue.options = {} Vue.mixin = function (mixin ) { this .options = mergeOptions(this .options, mixin) return this } }
utils.js
中实现 mergeOptions 方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 const strats = {}const LIFECYCLE = [ "beforeCreated" , "created" ] LIFECYCLE.forEach(key => { strats[key] = function (p, c ) { if (c) { if (p) { return p.concat(c) } else { return [c] } } else { return p } } }) export function mergeOptions (parent, child ) { const options = {} for (const key in parent) { mergeField(key) } for (const key in child) { if (!parent.hasOwnProperty(key)) { mergeField(key) } } function mergeField (key ) { if (strats[key]) { options[key] = strats[key](parent[key], child[key]) } else { options[key] = child[key] || parent[key] } } return options }
然后在 init.js 中进行属性合并和Hook调用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 import {initStatus} from "./state.js"; import {compilerToFunction} from "./compile/index.js"; import {mountComponent} from "./lifecycle.js"; +import {mergeOptions} from "./utils.js"; export function initMixin(Vue){ // 给Vue原型添加一个初始化方法 Vue.prototype._init = function (options){ const vm = this + // this.constructor就是当前的大Vue,获取的是Vue上的静态属性 + // this.constructor.options 拿到的就是mixin合并后的数据 + // 然后再把用户写的options和mixin中的进行再次合并 + vm.$options = mergeOptions(this.constructor.options,options) + // 初始化之前调用beforeCreated + callHook(vm,"beforeCreated") + // 初始化状态 + initStatus(vm) + // 初始化之后调用created + callHook(vm,"created") // 解析模板字符串 if(vm.$options.el){ vm.$mount(vm.$options.el) } } // 在原型链上添加$mount方法,用户获取页面模板 Vue.prototype.$mount = function (el){ let template; const vm = this el = document.querySelector(el) const opts = vm.$options // 判断配置中是否已经存在template,如果没有,则根据el获取页面模板 if(!opts.render){ if(!opts.template && opts.el){ // 拿到模板字符串 template = el.outerHTML } if(opts.template){ template = opts.template } if(template){ // 这里拿到模板开始进行模板编译 opts.render = compilerToFunction(template) } } // 有了render函数,开始对组件进行挂载 mountComponent(vm,el) } } +function callHook(vm,hook){ + // 拿到用户传入的钩子函数 + const handlers = vm.$options[hook] + if(handlers){ + // 遍历钩子函数,执行钩子函数 + for(let i=0;i<handlers.length;i++){ + handlers[i].call(vm) + } + } +}
测试 Vue.mixin
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 Vue.mixin({ beforeCreated ( ) { console .log("beforeCreated" ) }, created ( ) { console .log(this .name,"--mixin" ) }, }) const vm = new Vue({ el: "#app" , data ( ) { return { name: "szx" , age: 18 , address: { price: 100 , name: "少林寺" }, hobby: ['each' , 'write' , {a : "tome" }] } }, created ( ) { console .log(this .name,"--vue" ) } })
查看控制台打印
实现数组更新 我们之前给每一个属性都加了一个dep,实现依赖收集,但是如果这个属性值是一个对象类型的话,当我们不改变这个属性的引用地址,只是改变对象属性值,比如给数组push一个数据,不会改变原来的引用地址。这样的话页面就无法实现更新。
我们可以判断一下,当属性值是一个对象类型的时候,给这个对象本身也添加一个dep,当读取这个属性值的时候,进行一下依赖收集,如果是一个数组的话,当调用完push等方法时,在我们重写的方法哪里再执行一个更新就可以了。
下面是代码实现
修改 observe/index.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 import {newArrayProperty} from "./array.js" ;import Dep from "./dep.js" ;class Observer { constructor (data ) { + this .dep = new Dep() Object .defineProperty(data,"__ob__" ,{ value:this , enumerable:false }) if (Array .isArray(data)){ data.__proto__ = newArrayProperty this .observeArray(data) }else { this .walk(data) } } walk (data ) { Object .keys(data).forEach(key => { defineReactive(data,key,data[key]) }) } observeArray (data ) { data.forEach(item => observe(item)) } } +function dependArr (array ) { + array.forEach(item => { + + item.__ob__ && item.__ob__.dep.depend() + if (Array .isArray(item)){ + dependArr(item) + } + }) +} export function defineReactive (target,key,value ) { + const childOb = observe(value) let dep = new Dep(); Object .defineProperty(target,key,{ get ( ) { if (Dep.target){ dep.depend() + if (childOb){ + childOb.dep.depend() + if (Array .isArray(value)){ + dependArr(value) + } + } } return value }, set (newValue ) { if (newValue === value) return value = newValue dep.notify() } }) } export function observe (data ) { if (typeof data !== "object" || data === null ) return if (data.__ob__){ return data.__ob__ } return new Observer(data) }
实现效果
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 const vm = new Vue({ el: "#app" , data ( ) { return { name: "szx" , age: 18 , address: { price: 100 , name: "少林寺" }, hobby: ["爬山" ,"玩游戏" ] } }, created ( ) { console .log(this .name,"--vue" ) } }) function addAge ( ) { vm.hobby.push("吃" ) }
点击后页面会自动更新,并且控制台打印了一次更新视图
实现计算属性 首先添加computed计算属性
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 const vm = new Vue({ el: "#app" , data ( ) { return { name: "szx" , age: 18 , address: { price: 100 , name: "少林寺" }, hobby: ["爬山" ,"玩游戏" ] } }, created ( ) { console .log(this .name,"--vue" ) }, computed:{ fullname ( ) { console .log("调用计算属性" ) return this .name + this .age } } })
找到 state.js 文件,添加如下代码,添加针对 computed 属性的处理逻辑
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 import {observe} from "./observe/index.js" ;export function initStatus (vm ) { const opt = vm.$options if (opt.data){ initData(vm) } if (opt.computed){ initComputed(vm) } } function initComputed (vm ) { let computed = vm.$options.computed Object .keys(computed).forEach(key => { defineComputed(vm,key,computed) }) } function defineComputed (target,key,computed ) { let getter = typeof computed[key] === "function" ? computed[key] : computed[key].get let setter = computed[key].set || (()=> {}) Object .defineProperty(target,key,{ get:getter, set:setter }) }
现在我们就可以在页面上使用
1 2 3 <span > {{fullname}} {{fullname}} {{fullname}} </span >
但是会发现执行了三次计算属性的方法,在真正的vue中,计算属性是带有缓存的。我们可以定义一个标识,当执行完一次计算属性方法后,把这个标识改掉,下次再次调用计算属性时,从缓存获取
修改 initComputed 方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 function initComputed (vm ) { let computed = vm.$options.computed const computedWatchers = vm._computedWatchers = {} Object .keys(computed).forEach(key => { let getter = typeof computed[key] === "function" ? computed[key] : computed[key].get computedWatchers[key] = new Watcher(vm,getter,{lazy :true }) defineComputed(vm,key,computed) }) } function defineComputed (target,key,computed ) { let setter = computed[key].set || (()=> {}) Object .defineProperty(target,key,{ get:createComputedGetter(key), set:setter }) } function createComputedGetter (key ) { return function ( ) { let watcher = this ._computedWatchers[key] if (watcher.dirty){ watcher.evaluate() } return watcher.value } }
修改 Watcher.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 import Dep from "./dep.js" ;let id = 0 class Watcher { constructor (vm,fn,options = {} ) { this .id = id++ this .vm = vm this .getter = fn this .depts = [] this .deptSet = new Set () this .lazy = options.lazy this .dirty = this .lazy this .dirty ? undefined : this .get() } evaluate ( ) { this .value = this .get() this .dirty = false } get ( ) { pushWatcher(this ) let value = this .getter.call(this .vm) popWatcher() return value } } let stack = []function pushWatcher (watcher ) { stack.push(watcher) Dep.target = watcher } function popWatcher ( ) { stack.pop() Dep.target = stack[stack.length-1 ] } export default Watcher
上面我们给每一个计算属性绑定了一个计算watcher,并且添加了一个lazy标记,然后再watcher中吧dirty的值默认等于这个标记,同时添加一个evaluate方法,专门处理计算属性的返回值
现在我们页面上使用三次计算属性,但是只会执行一次
现在当我们更改依赖的属性时,页面不会发生变化
这是为什么呢?这是因为目前计算属性中依赖的属性中的dep绑定是的计算Watcher,并不是渲染Watcher,当我们改变了计算属性依赖值时,通知的只是计算属性Watcher,所以不会引起页面的渲染。这就需要同时去触发渲染Watcher。
在 createComputedGetter
方法中增加一个判断,判断Dep.target是否还有值,有值就表示计算属性的watcher出栈后,还有一个渲染watcher,调用watcher中的depend方法,获取计算属性watcher所有的属性,也就是每一个dep,遍历这些dep,去同时吧渲染watcher添加到这些计算属性所依赖的dep的订阅者中,这样当这些依赖的值发生变化时,就会通知到渲染watcher,从而去更新页面。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 function createComputedGetter (key ) { return function ( ) { let watcher = this ._computedWatchers[key] if (watcher.dirty){ watcher.evaluate() } if (Dep.target){ watcher.depend() } return watcher.value } }
然后再 Watcher 中添加 depend 方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 import Dep from "./dep.js" ;let id = 0 class Watcher { constructor (vm,fn,options = {} ) { this .id = id++ this .vm = vm this .getter = fn this .depts = [] this .deptSet = new Set () this .lazy = options.lazy this .dirty = this .lazy this .dirty ? undefined : this .get() } evaluate ( ) { this .value = this .get() this .dirty = false } depend ( ) { let i = this .depts.length while (i--){ this .depts[i].depend() } } } export default Watcher
现在当修改了计算属性所依赖的属性值时,会更新视图。然后重新调用一次计算属性
实现watch监听 watch可以理解为一个自定义的观察者watcher,当观察的属性发生变化时,执行对应的回调即可
首先新增一个全局 $watch
src/index.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 import {initMixin} from "./init"; import {initLifeCycle} from "./lifecycle.js"; import Watcher, {nextTick} from "./observe/watcher.js"; import {initGlobalApi} from "./globalApi.js"; function Vue(options) { this._init(options) } Vue.prototype.$nextTick = nextTick initMixin(Vue) initLifeCycle(Vue) initGlobalApi(Vue) + Vue.prototype.$watch = function (expOrFn, cb) { + new Watcher(this, expOrFn, {user:true},cb) + } export default Vue
然后再初始化状态,增加一个初始化watch的方法
src/state.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 import {observe} from "./observe/index.js" ;import Watcher from "./observe/watcher.js" ;import Dep from "./observe/dep.js" ;export function initStatus (vm ) { const opt = vm.$options if (opt.data){ initData(vm) } if (opt.computed){ initComputed(vm) } if (opt.watch){ initWatch(vm) } } function initWatch (vm ) { let watch = vm.$options.watch for (const watchKey in watch) { let handle = watch[watchKey] if (Array .isArray(handle)){ for (let handleElement of handle) { createWatcher(vm,watchKey,handleElement) } }else { createWatcher(vm,watchKey,handle) } } } function createWatcher (vm,keyOrFn,handle ) { vm.$watch(keyOrFn,handle) }
然后修改Watcher类,当所监听的值发生变化时触发回调
src/observe/watcher.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 import Dep from "./dep.js"; let id = 0 class Watcher{ constructor(vm,keyOrFn,options = {},cb) { this.id = id++ // 唯一ID this.vm = vm + // 如果是一个字符串吗,则包装成一个方法 + if(typeof keyOrFn === 'string'){ + this.getter = function (){ + return vm[keyOrFn] + } + }else{ + this.getter = keyOrFn // 这里存放多个dep,一个Watcher对应多个deps + } this.depts = [] this.deptSet = new Set() this.lazy = options.lazy // 用作计算属性的缓存,标记是否需要重新计算 this.dirty = this.lazy // 由于watcher会默认执行一次get,会渲染一次页面,但是计算属性不需要一上来就执行渲染 // 所以这里判断,如果dirty为true,则不执行get,只有当dirty为false的时候才执行get,渲染页面 + this.value = this.dirty ? undefined : this.get() + // 区分是否为用户自定义watcher + this.user = options.user + // 拿到watcher的回调 + this.cb = cb } // ....省略其他代码 run(){ console.log('更新视图') + let oldVal = this.value + let newVal = this.getter() + // 判断是否是用户自定义的watcher + if(this.user){ + this.cb.call(this.vm,newVal,oldVal) + } } } // ....省略其他代码 export default Watcher
实现基本的diff算法 首先吧 src/index.js
中的 $nextTick
和 $watch
放在 src/state.js
文件中,并封装在 initStateMixin 方法内,并且导出
src/state.js
1 2 3 4 5 6 7 8 9 10 import Watcher, {nextTick} from "./observe/watcher.js" ;export function initStateMixin (Vue ) { Vue.prototype.$nextTick = nextTick Vue.prototype.$watch = function (expOrFn, cb ) { new Watcher(this , expOrFn, {user :true },cb) } }
在 src/index.js
导出并使用,并且下面添加了diff的测试代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 import {initMixin} from "./init" ;import {initLifeCycle} from "./lifecycle.js" ;import {initGlobalApi} from "./globalApi.js" ;import {initStateMixin} from "./state.js" ;import {compilerToFunction} from "./compile/index.js" ;import {createEle, patch} from "./vdom/patch.js" ;function Vue (options ) { this ._init(options) } initMixin(Vue) initLifeCycle(Vue) initGlobalApi(Vue) initStateMixin(Vue) let render1 = compilerToFunction("<div style='color: red'></div>" )let vm1 = new Vue({data :{name :"张三" }})let prevVNode = render1.call(vm1)let el = createEle(prevVNode)document .body.appendChild(el)let render2 = compilerToFunction(`<div style='background-color: blue;color: white'> <h2>{{name}}</h2> <h3>{{name}}</h3> </div>` )let vm2 = new Vue({data :{name :"李四" }})let newVNode = render2.call(vm2)setTimeout (()=> { console .log(prevVNode) console .log(newVNode) patch(prevVNode,newVNode) },1000 ) export default Vue
上面代码生成了两个虚拟节点,然后倒计时1秒后进行更新
在 src/vdom/patch.js
中对节点进行比较
下面的代码在patchVNode完成新节点和旧节点的对比
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 import {isSameVNode} from "./index.js" ;export function patch (oldVNode,newVNode ) { const isRealEle = oldVNode.nodeType; if (isRealEle){ const elm = oldVNode const parentElm = elm.parentNode let newRealEl = createEle(newVNode) parentElm.insertBefore(newRealEl,elm.nextSibling) parentElm.removeChild(elm) }else { patchVNode(oldVNode,newVNode) } } export function createEle (vnode ) { let {tag,data,children,text} = vnode if (typeof tag === "string" ){ vnode.el = document .createElement(tag) patchProps(vnode.el,{},data) children.forEach(child => { child && vnode.el.appendChild(createEle(child)) }) }else { vnode.el = document .createTextNode(text) } return vnode.el } function patchProps (el,oldProps = {},props ) { let oldPropsStyle = oldProps.style let newPropsStyle = props.style for (const key in oldPropsStyle) { if (!newPropsStyle[key]){ el.style[key] = "" } } for (const key in oldProps) { if (!props[key]){ el.removeAttribute(key) } } for (const key in props) { if (key === "style" ){ Object .keys(props.style).forEach(sty => { el.style[sty] = props.style[sty] }) }else { el.setAttribute(key,props[key]) } } } function patchVNode (oldVNode,newVNode ) { if (!isSameVNode(oldVNode,newVNode)){ let el = createEle(newVNode) oldVNode.el.parentNode.replaceChild(el,oldVNode.el) return el } let el = newVNode.el = oldVNode.el if (!oldVNode.tag){ if (oldVNode.text !== newVNode.text){ el.textContent = newVNode.text } } patchProps(el,oldVNode.data,newVNode.data) let oldVNodeChildren = oldVNode.children || [] let newVNodeChildren = newVNode.children || [] if (oldVNodeChildren.length > 0 && newVNodeChildren.length > 0 ){ console .log("进行完整的diff算法" ) }else if (newVNodeChildren.length > 0 ){ mountChildren(el,newVNodeChildren) }else if (oldVNodeChildren.length > 0 ){ unMountChildren(el,oldVNodeChildren) } return el } function mountChildren (el,children ) { for (const child of children) { el.appendChild(createEle(child)) } } function unMountChildren (el,children ) { el.innerHTML = "" }
实现完整的diff算法 这里我们来完成当旧节点和新节点都有子元素时,进行互相对比。
在Vue2中使用了双指针来进行子元素之间的对比,一个指针指向第一个节点,一个指针指向最后一个节点,比较一次后,首指针往后移动一位,当首指针大于尾指针时,比较结束
patch.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 import {isSameVNode} from "./index.js" ;export function patch (oldVNode,newVNode ) { const isRealEle = oldVNode.nodeType; if (isRealEle){ const elm = oldVNode const parentElm = elm.parentNode let newRealEl = createEle(newVNode) parentElm.insertBefore(newRealEl,elm.nextSibling) parentElm.removeChild(elm) }else { patchVNode(oldVNode,newVNode) } } export function createEle (vnode ) { let {tag,data,children,text} = vnode if (typeof tag === "string" ){ vnode.el = document .createElement(tag) patchProps(vnode.el,{},data) children.forEach(child => { child && vnode.el.appendChild(createEle(child)) }) }else { vnode.el = document .createTextNode(text) } return vnode.el } function patchProps (el,oldProps = {},props = {} ) { let oldPropsStyle = oldProps.style let newPropsStyle = props.style for (const key in oldPropsStyle) { if (!newPropsStyle[key]){ el.style[key] = "" } } for (const key in oldProps) { if (!props[key]){ el.removeAttribute(key) } } for (const key in props) { if (key === "style" ){ Object .keys(props.style).forEach(sty => { el.style[sty] = props.style[sty] }) }else { el.setAttribute(key,props[key]) } } } function patchVNode (oldVNode,newVNode ) { if (!isSameVNode(oldVNode,newVNode)){ console .log(oldVNode,'oldVNode' ) console .log(newVNode,'newVNode' ) let el = createEle(newVNode) oldVNode.el.parentNode.replaceChild(el,oldVNode.el) return el } let el = newVNode.el = oldVNode.el if (!oldVNode.tag){ if (oldVNode.el.text !== newVNode.text){ oldVNode.el.text = newVNode.text } } patchProps(el,oldVNode.data,newVNode.data) let oldVNodeChildren = oldVNode.children || [] let newVNodeChildren = newVNode.children || [] if (oldVNodeChildren.length > 0 && newVNodeChildren.length > 0 ){ let oldStartIndex = 0 let oldEndIndex = oldVNodeChildren.length - 1 let oldStartNode = oldVNodeChildren[0 ] let oldEndNode = oldVNodeChildren[oldEndIndex] let newStartIndex = 0 let newEndIndex = newVNodeChildren.length - 1 let newStartNode = newVNodeChildren[0 ] let newEndNode = newVNodeChildren[newEndIndex] let nodeMap = {} oldVNodeChildren.forEach((child,index )=> { nodeMap[child.key] = index }) while (oldStartIndex <= oldEndIndex && newStartIndex <= newEndIndex){ if (!oldStartNode){ oldStartNode = oldVNodeChildren[++oldStartIndex] }else if (!oldEndNode){ oldEndNode = oldVNodeChildren[--oldEndIndex] } else if (isSameVNode(oldStartNode,newStartNode)){ patchVNode(oldStartNode,newStartNode) oldStartNode = oldVNodeChildren[++oldStartIndex] newStartNode = newVNodeChildren[++newStartIndex] } else if (isSameVNode(oldEndNode,newEndNode)){ patchVNode(oldEndNode,newEndNode) oldEndNode = oldVNodeChildren[--oldEndIndex] newEndNode = newVNodeChildren[--newEndIndex] } else if (isSameVNode(oldEndNode,newStartNode)){ patchVNode(oldEndNode,newStartNode) el.insertBefore(oldEndNode.el,oldStartNode.el) oldEndNode = oldVNodeChildren[--oldEndIndex] newStartNode = newVNodeChildren[++newStartIndex] } else if (isSameVNode(oldStartNode,newEndNode)){ patchVNode(oldStartNode,newEndNode) el.insertBefore(oldStartNode.el,oldEndNode.el.nextSibling) oldStartNode = oldVNodeChildren[++oldStartIndex] newEndNode = newVNodeChildren[--newEndIndex] }else { let oldNodeIndex = nodeMap[newStartNode.key] if (oldNodeIndex !== undefined ){ let moveNode = oldVNodeChildren[oldNodeIndex] el.insertBefore(moveNode.el,oldStartNode.el) oldVNodeChildren[oldNodeIndex] = undefined patchVNode(moveNode,newStartNode) }else { el.insertBefore(createEle(newStartNode),oldStartNode.el) } newStartNode = newVNodeChildren[++newStartIndex] } } if (newStartIndex <= newEndIndex){ console .log("1" ) for (let i = newStartIndex; i <= newEndIndex; i++) { let anchor = newVNodeChildren[newEndIndex + 1 ] ? newVNodeChildren[newEndIndex + 1 ].el : null el.insertBefore(createEle(newVNodeChildren[i]),anchor) } } if (oldStartIndex <= oldEndIndex){ for (let i = oldStartIndex; i <= oldEndIndex; i++) { let chilEl = oldVNodeChildren[i] chilEl && el.removeChild(chilEl.el) } } }else if (newVNodeChildren.length > 0 ){ mountChildren(el,newVNodeChildren) }else if (oldVNodeChildren.length > 0 ){ unMountChildren(el,oldVNodeChildren) } return el } function mountChildren (el,children ) { for (const child of children) { el.appendChild(createEle(child)) } } function unMountChildren (el,children ) { el.innerHTML = "" }
测试一下,手动的编写两个虚拟节点进行比对
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 let render1 = compilerToFunction(`<div style='color: red'> <li key="a">a</li> <li key="b">b</li> <li key="c">c</li> <li key="d">d</li> </div>` )let vm1 = new Vue({data :{name :"张三" }})let prevVNode = render1.call(vm1)let el = createEle(prevVNode)document .body.appendChild(el)let render2 = compilerToFunction(`<div style='background-color: blue;color: white'> <li key="f">f</li> <li key="e">e</li> <li key="c">c</li> <li key="n">n</li> <li key="a">a</li> <li key="m">m</li> <li key="j">j</li> </div>` )let vm2 = new Vue({data :{name :"李四" }})let newVNode = render2.call(vm2)setTimeout (()=> { console .log(prevVNode) console .log(newVNode) patch(prevVNode,newVNode) },1000 )
倒计时一秒后会自动变成新的
但是我们肯定不能使用这种方式来实现页面的更新和diff,需要在修改完数据后,在update中进行新旧节点的diff
修改 lifecycle.js
文件中的 update 方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 Vue.prototype._update = function (vnode ) { const vm = this const el = vm.$el const preVNode = vm._vnode vm._vnode = vnode if (preVNode){ vm.$el = patch(preVNode,vnode) }else { vm.$el = patch(el,vnode) } }
查看效果
通过动画我们可以看到每次更新时只有里面的文字变化,其他元素并不会重新渲染
自定义组件实现原理 vue中可以声明自定义组件和全局组件,当自定义组件和全局组件重名时,会优先使用自定义组件。
在源码中,主要靠 Vue.extend 方法来实现
例如如下写法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 Vue.component("my-button" ,{ template:"<button>全局的组件</button>" }) let Sub = Vue.extend({ template:"<button>子组件 <my-button></my-button></button>" , components:{ "my-button" :{ template:"<button>子组件自己声明的button</button>" } } }) new Sub().$mount("#app" )
页面展示的效果
我们来实现这个源码
在 globalApi.js
文件中添加方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 import {mergeOptions} from "./utils.js" ;export function initGlobalApi (Vue ) { Vue.options = { _base:Vue } Vue.mixin = function (mixin ) { this .options = mergeOptions(this .options, mixin) return this } Vue.extend = function (options ) { function Sub (options = {} ) { this ._init(options) } Sub.prototype = Object .create(Vue.prototype) Sub.prototype.constructor = Sub Sub.options = mergeOptions(Vue.options,options) return Sub } Vue.options.components = {} Vue.component = function (id,options ) { options = typeof options === "function" ? options : Vue.extend(options) Vue.options.components[id] = options } }
在 utils.js
文件中添加组件合并策略,实现先找自身声明的组件,找不到再去原型链上找全局声明的组件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 const strats = {}const LIFECYCLE = [ "beforeCreated" , "created" ] LIFECYCLE.forEach(key => { strats[key] = function (p, c ) { if (c) { if (p) { return p.concat(c) } else { return [c] } } else { return p } } }) strats.components = function (parentVal, childVal ) { const res = Object .create(parentVal) if (childVal){ for (const key in childVal) { res[key] = childVal[key] } } return res } export function mergeOptions (parent, child ) { const options = {} for (const key in parent) { mergeField(key) } for (const key in child) { if (!parent.hasOwnProperty(key)) { mergeField(key) } } function mergeField (key ) { if (strats[key]) { options[key] = strats[key](parent[key], child[key]) } else { options[key] = child[key] || parent[key] } } return options }
这一步实现了组件按照原型链查找,通过打断点可以看到
接着修改 src/vdom/index.js
文件,增加创建自定义组件的虚拟节点
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 let isReservedTag = (tag ) => { return ["a" , "div" , "span" , "button" , "ul" , "li" , "h1" , "h2" , "h3" , "h4" , "h5" , "h6" , "p" , "input" , "img" ].includes(tag) } export function createElementVNode (vm, tag, prop, ...children ) { if (!prop) { prop = {} } let key = prop.key if (key) { delete prop.key } if (isReservedTag(tag)) { return vnode(vm, tag, prop, key, children, undefined ) } else { return createTemplateVNode(vm, tag, prop, key, children) } } function createTemplateVNode (vm, tag, data, key, children ) { let Core = vm.$options.components[tag] if (typeof Core === "object" ) { Core = vm.$options._base.extend(Core) } data.hook = { init ( ) { } } return vnode(vm, tag, data, key, children = [], undefined , Core) } export function createTextVNode (vm, text ) { return vnode(vm, undefined , undefined , undefined , undefined , text) } function vnode (vm, tag, data, key, children = [], text, componentsOptions ) { children = children.filter(Boolean ) return { vm, tag, data, key, children, text, componentsOptions } } export function isSameVNode (oldVNode, newVNode ) { return oldVNode.tag === newVNode.tag && oldVNode.key === newVNode.key }
实现组件渲染功能 上面我们根据tag判断是否是一个组件,并且添加了一个 createTemplateVNode 方法,返回组件的虚拟节点vnode。
然后需要在 src/vdom/patch.js
文件的 createEle 生成真实节点的方法中添加判断,是否是虚拟节点
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 function createComponent (vnode ) { let i = vnode.data if ((i=i.hook) && (i=i.init)){ i(vnode) } if (vnode.componentsInstance){ return true } } export function createEle (vnode ) { let {tag,data,children,text} = vnode if (typeof tag === "string" ){ if (createComponent(vnode)){ return vnode.componentsInstance.$el } vnode.el = document .createElement(tag) patchProps(vnode.el,{},data) children.forEach(child => { child && vnode.el.appendChild(createEle(child)) }) }else { vnode.el = document .createTextNode(text) } return vnode.el }
在 createComponent 方法中就会去调用在上面 createTemplateVNode 方法中定义的 init 方法,并把当前的 vnode 传递过去
这时需要在init方法中接收这个vnode,并去new 这个 vnode 的 componentsOptions 中的 Core,这里的Core也就是 Vue.extend
修改 src/vdom/index.js
中的 createTemplateVNode 方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 function createTemplateVNode (vm, tag, data, key, children ) { let Core = vm.$options.components[tag] if (typeof Core === "object" ) { Core = vm.$options._base.extend(Core) } data.hook = { init (vnode ) { let instance = vnode.componentsInstance = new vnode.componentsOptions.Core instance.$mount() } } return vnode(vm, tag, data, key, children = [], undefined , {Core}) }
new 完 Core 后返回的实例同时赋值给当前vnode的componentsInstance上和局部变量instance
然后使用 instance 再去调用 $mount 方法,会触发 patch 方法,但是这里并没有传递参数,所以就需要在 patch 方法中添加一个判断,如果没有旧节点,直接创建新的节点并返回
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 export function patch(oldVNode,newVNode){ + if(!oldVNode){ + return createEle(newVNode) + } // 判断是否是一个真实元素,如果是真实DOM会返回1 const isRealEle = oldVNode.nodeType; // 初次渲染 if(isRealEle){ // 获取真实元素 const elm = oldVNode // 获取真实元素的父元素 const parentElm = elm.parentNode // 创建新的虚拟节点 let newRealEl = createEle(newVNode) // 把新的虚拟节点插入到真实元素后面 parentElm.insertBefore(newRealEl,elm.nextSibling) // 然后删除之前的DOM parentElm.removeChild(elm) // 返回渲染后的虚拟节点 return newRealEl }else{ return patchVNode(oldVNode,newVNode) } }
这是用我们自己的vue.js来看一下实现的效果
1 2 3 4 5 6 7 8 9 10 11 <body > <div id ="app" > <ul > <li > {{age}}</li > <li > {{name}}</li > </ul > <button onclick ="updateAge()" > 更新</button > </div > </body > <script src ="./vue.js" > </script >
1 2 3 4 5 6 7 8 9 10 11 12 13 14 Vue.component("my-button" ,{ template:"<button>全局的组件</button>" }) let Sub = Vue.extend({ template:"<button>子组件 <my-button></my-button></button>" , components:{ "my-button" :{ template:"<button>子组件自己声明的button</button>" } } }) new Sub().$mount("#app" )
总结:
创建子类构造函数的时候,会将全局的组件和自己身上定义的组件进行合并。(组件的合并,会优先查找自己身上的,找不到再去找全局的)
组件的渲染:开始渲染的时候组件会编译组件的模板(template 属性对应的 html)变成render函数,然后调用 render函数
createrElementVnode 会根据 tag 类型判断是否是自定义组件,如果是组件会创造出组件对应的虚拟节点(给组件增加一个初始化钩子,增加componentOptions选项 { Core })
然后再创建组件的真实节点时。需要 new Core,然后使用返回的实例在去调用 $mount() 方法就可以完成组件的挂载