上篇文章结尾代码:
function genProps(attrs){
let str=''
for(let i=0;i<attrs.length;i++){
let attr=attrs[i]
if(attr.name==='style'){
let styleObj={}
attr.value.replace(/([^;:]+):([^;:]+)/g,function(){
styleObj[arguments[1]]=argument[2]
})
attr.value=styleObj
}
str+=`${attr.name}:${JSON.stringify(attr.value)}`
}
return `{${str.slice(0,-1)}}`
}
function gen(el){
if(el.type==1){
return generate(el)
}else{
let text=el.text
if(!defaultTagRE.test(text)){
return '_v('${text}')'
}else{
// 进行拆分 'hello'+arr+'world'
let tokens=[]
let match
let lastIndex=0
while(match==defaultTagRE.exec(text)){
let index=match.index
if(index>lastIndex){
tokens.push(JSON.stringify(text.slice(lastIndex)))
}
lastIndex=index+match[0].length
}
if(lastIndex<text.length){
tokens.push(JSON.stringify(text.slice(lastIndex)))
}
return `_v(${tokens.join('+')})`
}
}
}
// 生成儿子
function genChildren(){
let children=el.children
if(children){
return children.map(c=>gen(c)).join(",")
}
return false
}
// html字符串转化为字符串
export function generate(root){ //_c('div',{id:'app',a:1},_c('span',{},'world'),_v())
console.log('------------',root)
let code=`_c('${root.tag}',${
el.attrs.length?:'undefined'
}${
children?`,${children}`:
})` // 表明我们创建的标签名
return code
}
这一段转换最关键的部分是语法之间的转化,很大意义上并不是代码上的转换。这会我们就需要看用户是否传入了el属性,如果没传入的话可能传入了template,如果说template也没有传递的话我们将做如下的检查判断
书接上文,我们将一段文本的标签放入了_c(),文本放入了_v(),马斯塔奇放入了_s().我们得到了如下代码:
export function compileToFunction(template){
// 生成代码
let code=codegen(root)
let render=new Function(`with(this){return ${code}}`)
console.log(render.toString())
render.call(vm)
}
在compile库下新建一个文件,命名为generator.js
template中的马斯塔器this问题
很多刚开始写Vue的小白老是会犯一个非常严重的错误,那就是因不应该在template的马斯塔器里面调用data里面的属性上用this。这里可以非常负责的说,在Vue2中是完全可以的,这里可以牵扯出一个JavaScript函数with的问题。以下面这个函数举例:
fn({arr:[1,2,3]})
function fn(vm){
console.log(arr)
}
上面这个function是内部是没有定义arr的。但是我们可以直接使用vm这个函数内部的arr。这会我们这个this指向的是vm。我们直接使用arr可以等价为this.arr。在Vue template的马斯塔器语法中这个类似
那么引入到我们上面的
new Function(with(this){return ${code}})我们这里的this其实指向的就是template了,然后code就是template对象里面的code。然后这样写完之后,我们就解决了一个问题,一般用户是不会传render方法的,除非它有渲染自定义h函数的需求。但是用户一定会传el或template。因为不传这两个我们就会给他报错。我们现在在做的,一直就是我们上篇文章说的。我们要将Vue的template和el都转成render。然后生成虚拟dom,我们再去处理。好的,现在我们来编写下这部分的关键代码;
Vue.prototype.$mount=function(el){
const vm=this
const options=vm.$options
el=document.querySelector(el)
if(!options.render){
let template=options.template
if(!template&&el){
template=el.outerHTML
let render=compileToFunction(template)
options.render=render
}
}
// options.render就是渲染函数
console.log(options.render) // 调用render方法,渲染成真实dom,替换掉页面内容
mountComponent(vm,el) // 组件的挂载流程
}
实现挂载流程
function mountComponent(vm,el){
// 数据变化后会再次调用此方法
let updateComponent=()=>{
// 调用render函数生成虚拟dom
vm._update(vm._render()) // 后续更新可以调用updateComponent
// 用虚拟dom 生成真实dom
}
updateComponent()
}
在创建实例的时候扩展原型的update和render方法
// Vue的入口文件 src/index.js
import {initMixin} from './init'
import {lifecycleMixin} from './lifecycle'
function Vue(options){
this._init(options)
}
initMixin(Vue)
renderMixin(Vue)
lifecycleMixin(Vue)
export default Vue;
//src/render.js
export function renderMixin(Vue){
Vue.prototype._render=function(){
const vm=this
let render=vm.$options.render // 就是我们转义,解析,上篇文章得到的function,也可能是用户写的
let vnode=render.call(vm)
}
}
//src/lifecycle.js
export function lifecycleMixin(Vue){
Vue.prototype._update=function(vnode){
}
}
export function mountComponent(vm,el){
let updateComponent=()=>{
vm._update(vm._render())
}
updateComponent()
}
好的,现在我们重新回顾下,_C为解析标签,_v为解析文本,_s为解析马斯塔奇,我们经历了3000多个字,终于到达这个关键的地方了。
// 标签解析传入三个参数,标签名,属性,孩子
Vue.prototype._c=function(tagName,attr,...children){
return createElement(this,...arguments)
}
Vue.prototype._v=function(text){
return createTextElement(this,text)
}
Vue.prototype._s=function(val){
if(typeof val=='object'){
return JSON.stringify(val)
}
return val
}
// src/vdom/index.js
export function createElement(vm,tag,data={},...children){
return vnode(vm,tag,data,data.key,children,undefined)
}
export function createTextElement(vm,text){
return vnode(vm,undefined,undefined,undefined,undefined,text)
}
function vnode(vm,tag,data,key,children,text){
return {
vm,
tag,
data,
key,
children,
text,
}
}
将虚拟dom创建为真实dom
// src/lifecycle.js
import {patch} from './patch.js'
export function lifecycleMixin(Vue){
Vue.prototype._update=function(vnode){
const vm=this
// 核心的diff流程,第一次更新可能是真实节点,但是第二次进来一定是虚拟节点
patch(vm.$el,vnode)
}
}
export function mountComponent(vm,el){
let updateComponent=()=>{
vm._update(vm._render())
}
updateComponent()
}
// src/init.js
Vue.prototype.$mount=function(el){
const vm=this
const options=vm.$options
vm.$el=el
if(!options.render){
let template=options.template
if(!template&&el){
template=el.outerHTML
let render=compileToFunction(template)
options.render=render
}
}
mountComponent(vm,el)
}
//src/vnode/patch.js
export function patch(oldVnode,vnode){
if(oldVnode.type==1){
// 用vnode来生成真实dom,替换掉原本的dom元素
const parentElm=oldVnode.parentNode // 找到他的父亲
let elm=createElm(vnode) // 根据虚拟节点创建元素
parentElm.insertBefore(oldVnode)
parentElm.removeChild(oldVnode) 将这个元素给他删掉
}
}
function createElm(vnode){
let {tag,data,children,text,vm}=vnode
if(typeof vnode.tag=='string'){ // 它是一个元素
vnode.el=document.createElement(tag) // 虚拟节点会有一个el属性,对应真实节点
children.forEach(child=>{
vnode.el.appendChild(createElm(child))
})
}else{
vnode.el=document.createTextNode(text)
}
return vnode.el
}
虚拟节点通过上述逻辑转化为了真实节点
我正在参加「创意开发 投稿大赛」详情请看:掘金创意开发大赛来了!